#include "vanila/scanner.h"
#include "vanila/token.h"
#include "utils/charcheck.h"
#include <cstring>

namespace vanila
{

//! \brief init the Scanner!
//! \param[in] source source code's address
void Scanner::init(const char* source) noexcept
{
    this->_start = source;
    this->_current = source;
    this->_line = 1;
}

//! \brief scan a new token in after current pointer
Token Scanner::scanToken() noexcept
{
    // white character
    this->skipWhitecharacter();

    this->_start = this->_current;

    if (this->isAtEnd()) 
        return this->makeToken(TokenType::ENDOFFILE);

    char c = this->advance();

    // start as a legal identifier's begin character
    if (utils::isAlpha(c))
        return this->scanIdentifier();

    // start as a digit
    if (utils::isDigit(c))
        return this->scanNumber();

    switch (c)
    {
    // one character Token
    case '(': return this->makeToken(TokenType::LEFT_PAREN);
    case ')': return this->makeToken(TokenType::RIGHT_PAREN);
    case '[': return this->makeToken(TokenType::LEFT_BRACKET);
    case ']': return this->makeToken(TokenType::RIGHT_BRACKET);
    case '{': return this->makeToken(TokenType::LEFT_BRACE);
    case '}': return this->makeToken(TokenType::RIGHT_BRACE);
    case ';': return this->makeToken(TokenType::SEMICOLON);
    case ':': return this->makeToken(TokenType::COLON);
    case ',': return this->makeToken(TokenType::COMMA);
    case '.': return this->makeToken(TokenType::DOT);
    case '-': return this->makeToken(TokenType::MINUS);
    case '+': return this->makeToken(TokenType::PLUS);
    case '*': return this->makeToken(TokenType::STAR);
    case '/': return this->makeToken(TokenType::SLASH);

    // two characters Token, check the next character to dicide token type
    case '!':
        return this->makeToken( this->match('=') ? TokenType::BANG_EQUAL : TokenType::BANG );
    case '=':
        return this->makeToken( this->match('=') ? TokenType::EQUAL_EQUAL : TokenType::EQUAL );
    case '<':
        return this->makeToken( this->match('=') ? TokenType::LESS_EQUAL : TokenType::LESS );
    case '>':
        return this->makeToken( this->match('=') ? TokenType::GREATER_EQUAL : TokenType::GREATER );
    
    // literal token
    case '"': return scanString();
    }

    return this->errorToken(" Unexpected character.");
}

//! \brief check wheather current character is expected, is so current increase
//! \param[in] expected expect character
bool Scanner::match(char expected) noexcept
{
    if (this->isAtEnd() || (*this->_current) != expected)
        return false;
    this->_current++;
    return true; 
}

//! \brief skip the white character, such as ' ', '\t', '\r', '\n'
void Scanner::skipWhitecharacter() noexcept
{
    for (;;)
    {
        char c = this->peek();
        switch (c)
        {
        case ' ':
        case '\r':
        case '\t':
            // just jump
            this->advance();
            break;
        // for '\n', need to increase the line field!
        case '\n':
            this->_line++;
            this->advance();
            break;
        // comment
        case '/':
            if (this->peekNext() == '/')
                while (this->peek() != '\n' && !this->isAtEnd())
                    this->advance();
            else
                return;
            break;
        default:
            return;
        }
    }
}

//! \brief scan a number token after current character
Token Scanner::scanNumber() noexcept
{
    // jump all digit
    while (utils::isDigit(this->peek()))
        this->advance();

    // is a '.'?
    if (this->peek() == '.')
    {
        // is so, jump the '.'
        this->advance();

        // try to consume the digit after '.'
        while (utils::isDigit(this->peek()))
            this->advance();
        
        return this->makeToken(TokenType::DECIMAL);
    }

    // after digit, is not a '.'
    return this->makeToken(TokenType::INTEGER);
}

//! \brief scan a string token after current character
Token Scanner::scanString() noexcept
{
    // jump all character include two '"'
    while (this->peek() != '"' && !this->isAtEnd())
    {
        if (this->peek() == '\n')
            this->_line++;
        this->advance();
    }

    if (this->isAtEnd())
        return this->errorToken("Unterminated string.");
    
    // jump '"'
    this->advance();
    return this->makeToken(TokenType::STRING);
}

//! \brief scan a identifier after current character
Token Scanner::scanIdentifier() noexcept
{
    // the first character of identifier already been jumped!
    while (utils::isAlpha(this->peek()) || utils::isDigit(this->peek()))
        this->advance();
    return this->makeToken(this->getIdentifierType());
}

//! \brief get the identifier type of the string os start to current
TokenType Scanner::getIdentifierType() noexcept
{
    // a DFA to scann identifier
    switch (this->_start[0])
    {
    // some keyword which has unique satrt character
    case 'a': return this->checkKeyword(1, 2, "nd", TokenType::AND);
    case 'c': return this->checkKeyword(1, 4, "lass", TokenType::CLASS);
    case 'e': return this->checkKeyword(1, 3, "lse", TokenType::ELSE);
    case 'i': return this->checkKeyword(1, 1, "f", TokenType::IF);
    case 'n': return this->checkKeyword(1, 2, "il", TokenType::NIL);
    case 'o': return this->checkKeyword(1, 1, "r", TokenType::OR);
    case 'r': return this->checkKeyword(1, 5, "eturn", TokenType::RETURN);
    case 's': return this->checkKeyword(1, 4, "uper", TokenType::SUPER);
    case 'v': return this->checkKeyword(1, 2, "ar", TokenType::VAR);
    case 'w': return this->checkKeyword(1, 4, "hile", TokenType::WHILE);
    case 'f':
    {
        if (this->_current - this->_start > 1)
        {
            switch (this->_start[1])
            {
            case 'a': return this->checkKeyword(2, 3, "lse", TokenType::FALSE);
            case 'o': return this->checkKeyword(2, 1, "r", TokenType::FOR);
            case 'u': return this->checkKeyword(2, 1, "n", TokenType::FUN);
            }
        }
        break;
    }
    case 't':
    {
        if (this->_current - this->_start > 1)
        {
            switch (this->_start[1])
            {
            case 'h': return this->checkKeyword(2, 2, "is", TokenType::THIS);
            case 'r': return this->checkKeyword(2, 2, "ue", TokenType::TRUE);
            }
        }
        break;
    }
    }
    return TokenType::IDENTIFIER;
}

//! \brief check the after start position of _start, is meet the string: rest,
//! \brief is so, return type or IDENTIFIER
TokenType Scanner::checkKeyword(uint8_t start, uint8_t length, const char* rest, TokenType type) noexcept
{
    if (this->_current - this->_start == start + length &&
        memcmp(this->_start + start, rest, length) == 0)
        return type;
    return TokenType::IDENTIFIER;
}

}